package com.zunipe.ftpe;

import com.zunipe.ftpe.util.Utils;
import com.zunipe.ftpe.util.ZI18n;
import com.zunipe.ftpe.util.ZLog;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;

import java.io.*;
import java.util.LinkedList;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class FTPDownloader {
    private static final String TAG = "FTPDownloader";

    private FTPClientWrapper mFtpClientWrapper;
    private final Queue<DownloadTask> mDownloadQueue;
    private final ExecutorService executorService = new ThreadPoolExecutor(
            0, 1,
            5L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
    );
    private boolean mDownloading;
    private boolean mCanBreakTheResume;
    private boolean mShutdown;

    public FTPDownloader(FTPClientWrapper ftpClientWrapper) {
        this.mFtpClientWrapper = ftpClientWrapper;
        mDownloadQueue = new LinkedBlockingQueue<>();
    }

    public void setFtpClientWrapper(FTPClientWrapper ftpClientWrapper) {
        this.mFtpClientWrapper = ftpClientWrapper;
    }

    private File createFile(String fileName) throws IOException {
        File path = new File(Constants.DOWNLOAD_PATH);
        File file = new File(Constants.DOWNLOAD_PATH + File.separator + fileName);
        if (!path.exists()) {
            if (!path.mkdirs()) {
                ZLog.e(TAG, ZI18n.getString("downloader.error.failed_create_directory"));
                throw new IOException("Failed to create the download directory");
            }
        }
        if (!file.exists()) {
            if (!file.createNewFile()) {
                ZLog.e(TAG, ZI18n.getString("downloader.error.failed_create_file"));
                throw new IOException("Failed to create the download file");
            }
            mCanBreakTheResume = false;
        } else {
            mCanBreakTheResume = true;
        }
        return file;
    }

    public void stop() {
        mShutdown = true;
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    public void downloadFile(String remotePath, String fileName, OnDownloadComplete callback) {
        DownloadTask downloadTask = new DownloadTask(remotePath, fileName, callback);
        if (!mDownloadQueue.contains(downloadTask)) {
            ZLog.i(TAG, ZI18n.getString("downloader.info.add_queue"), fileName);
            mDownloadQueue.add(downloadTask);
        }
    }

    public void checkQueueAndStartDownload() {
        if (!mDownloadQueue.isEmpty() && !mDownloading) {
            mDownloading = true;
            startDownload();
        }
    }

    private void startDownload() {
        DownloadTask task = mDownloadQueue.poll();
        if (task != null) {
            String remotePath = task.getRemotePath();
            String fileName = task.getFileName();
            String completePath = remotePath + Constants.FTP_SEPARATOR + fileName;
            executorService.execute(() -> {
                Looper processMonitor = null;
                try {
                    FTPClient ftpClient = mFtpClientWrapper.getFtpClient();
                    FTPFile[] ftpFiles = ftpClient.listFiles(remotePath);
                    FTPFile targetFile = null;
                    for (FTPFile ftpFile : ftpFiles) {
                        if (fileName.equals(ftpFile.getName())) {
                            targetFile = ftpFile;
                            break;
                        }
                    }

                    if (targetFile != null) {
                        ZLog.i(TAG, ZI18n.getString("downloader.info.start_downloading"), completePath);
                        DownloadState downloadState;
                        File file = createFile(fileName + ".downloading");
                        // 触发断点续传
                        if (mCanBreakTheResume) {
                            long size = file.length();
                            downloadState = new DownloadState(targetFile.getSize(), size);
                            ftpClient.setRestartOffset(size);
                            ZLog.i(TAG, ZI18n.getString("downloader.info.trigger_breakpoint_resume"));
                        } else {
                            downloadState = new DownloadState(targetFile.getSize(), 0);
                            ftpClient.setRestartOffset(0);
                        }
                        processMonitor = new Looper(5, (looper) -> {
                            if (mShutdown) {
                                looper.stop();
                                return;
                            }
                            System.out.print(downloadState);
                        });
                        processMonitor.start();

                        try (OutputStream outputStream = new FileOutputStream(file, mCanBreakTheResume);
                             InputStream inputStream = ftpClient.retrieveFileStream(completePath)) {
                            byte[] buffer = new byte[4096];
                            int len;
                            while (downloadState.downloadSize < downloadState.fileSize &&
                                    (len = inputStream.read(buffer)) != -1) {
                                outputStream.write(buffer, 0, len);
                                downloadState.add(len);
                                if (mShutdown) {
                                    return;
                                }
                            }
                            if (downloadState.isSizeMatching()) {
                                long endTime = System.currentTimeMillis();
                                processMonitor.stop();
                                outputStream.close();
                                file.renameTo(new File(Constants.DOWNLOAD_PATH + File.separator + fileName));
                                System.out.print("\r");
                                task.getCallback().onDownloadComplete(fileName, endTime - downloadState.startTime);
                            } else {
                                System.out.print("\r");
                                ZLog.e(TAG, ZI18n.getString("downloader.error.file_size_not_match"), completePath);
                            }
                        }
                    } else {
                        ZLog.e(TAG, ZI18n.getString("downloader.error.target_file_not_found"), completePath);
                    }
                } catch (IOException e) {
                    ZLog.e(TAG, ZI18n.getString("downloader.error.failed_download"), completePath, e);
                } finally {
                    if (processMonitor != null) {
                        processMonitor.stop();
                    }
                    mDownloading = false;
                }
            });
        } else {
            mDownloading = false;
        }
    }

    public static class DownloadState {
        private static final int PROGRESS_BAR_LENGTH = 50;
        private final long fileSize;
        private long downloadSize;
        private final long startTime;
        private final LinkedList<Long> downloadQueue = new LinkedList<>();

        public DownloadState(long fileSize, long downloadSize) {
            this.fileSize = fileSize;
            this.downloadSize = downloadSize;
            this.startTime = System.currentTimeMillis();
        }

        public boolean isSizeMatching() {
            return fileSize == downloadSize;
        }

        @Override
        public String toString() {
            if (downloadQueue.size() >= 5) {
                downloadQueue.pollLast();
            }
            downloadQueue.push(downloadSize);
            Optional<Long> first = Optional.ofNullable(downloadQueue.peekFirst());
            Optional<Long> last = Optional.ofNullable(downloadQueue.peekLast());
            long size = first.orElse(0L) - last.orElse(0L);

            StringBuilder result = new StringBuilder();
            float percentage = (float) downloadSize / fileSize * 100;
            int progress = (int) (PROGRESS_BAR_LENGTH * percentage / 100);

            result.append("\r[");
            for (int i = 0; i < PROGRESS_BAR_LENGTH; i++) {
                if (i < progress) {
                    result.append("=");
                } else {
                    result.append(" ");
                }
            }

            result.append("] ").append(String.format("%.2f%%", percentage));
            result.append(" ").append(Utils.formatByte(downloadSize)).append("/").append(Utils.formatByte(fileSize));
            result.append(" ").append(Utils.formatByte(size)).append("/s").append("                 ");
            return result.toString();
        }

        public void add(long size) {
            downloadSize += size;
        }
    }
}
